/*
OpenDocumentMetadata is an Object representing the metadata in an
OpenDocument file.
Copyright (C) 2005  J. David Eisenberg
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
Author: J. David Eisenberg
Contact: catcode@catcode.com
 */
package com.catcode.odf;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import java.util.zip.ZipFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

/**
 * Analyze an OpenDocument meta file and create an
 * <code>OpenDocumentMetadata</code> object.
 *
 * This code depends upon there being no duplicate
 * element names between the Dublin Core and OpenDocument
 * meta namespaces. If there are, this code will break.
 * 
 * This code also depends upon there being no nested elements
 * in the <code>&lt;office:meta&gt;</code> element.
 * 
 *	@author		J. David Eisenberg
 *	@version	0.2, 2005-11-09
 */
public class ODFMetaFileAnalyzer {

	protected String officeNamespace;
	protected String dcNamespace;
	protected String metaNamespace;
	private static final String OPENDOCUMENT_URI =
			"urn:oasis:names:tc:opendocument:xmlns:office:1.0";
	private static final String DC_URI =
			"http://purl.org/dc/elements/1.1/";
	private static final String META_URI =
			"urn:oasis:names:tc:opendocument:xmlns:meta:1.0";
	private static final String STATISTICS =
			"document-statistic";
	private static final String USER_DEFINED =
			"user-defined";
	private static Class metadataClass = OpenDocumentMetadata.class;
	private static Class[] stringParameter = {String.class};
	private static Class[] intParameter = {int.class};

	/**
	 * Analyze the metadata in an <code>InputStream</code>.
	 *
	 * <p>Algorithm:</p>
	 * <ol>
	 * <li>Parse the input stream into a <code>Document</code></li>
	 * <li>From the root element, determine the namespace prefixes for
	 * that correspond to <code>office:</code>, <code>meta:</code>, and
	 * <code>dc:</code>.</li>
	 * <li>For each child element of the <code>&lt;office:meta&gt;</code>
	 * element, process it with the
	 * {@link #processElement(Element,OpenDocumentMetadata)
	 * processElement()} method, except for
	 * <code>&lt;meta:document-statistic&gt;</code>, which is handled with
	 * the {@link #processStatistic(Element,OpenDocumentMetadata)
	 * processStatistic()}, and
	 * <code>&lt;meta:user-defined&gt;</code>, which is handled with the
	 * {@link #processUserDefined(Element,OpenDocumentMetadata)
	 * processUserDefined()} method.
	 * </li>
	 * </ol>
	 *
	 * @param metaStream an <code>InputStream</code> that contains OpenDocument
	 * meta-information.
	 * @return an <code>OpenDocumentMetadata</code> structure that
	 * represents the file's meta information.
	 */
	public OpenDocumentMetadata analyzeMetadata(InputStream metaStream) {
		DocumentBuilder builder;
		Document doc;
		Node metaElement;
		OpenDocumentMetadata metadataResult;

		try {
			metadataResult = new OpenDocumentMetadata();
			builder =
					DocumentBuilderFactory.newInstance().newDocumentBuilder();
			doc = builder.parse(metaStream);
			findNamespaces(doc.getDocumentElement());
			metaElement = doc.getElementsByTagName(
					officeNamespace + "meta").item(0);
			if (metaElement != null) {
				metaElement = metaElement.getFirstChild();
				while (metaElement != null) {
					if (metaElement.getNodeType() == Node.ELEMENT_NODE) {
						String name = metaElement.getNodeName();
						if (name.equals(metaNamespace + STATISTICS)) {
							processStatistic((Element) metaElement,
									metadataResult);
						} else if (name.equals(metaNamespace + USER_DEFINED)) {
							processUserDefined((Element) metaElement,
									metadataResult);
						} else {
							processElement((Element) metaElement,
									metadataResult);
						}
					}
					metaElement = metaElement.getNextSibling();
				}
			}
		} catch (Exception e) {
			metadataResult = null;
		}

		return metadataResult;
	}

	/**
	 * Analyze the metadata in a <code>InputStream</code> which 
	 * is a .zip file.
	 *
	 * Written by Antti Jokipii to allow streaming of the
	 * .zip file
	 *
	 * @param inputStream a <code>InputStream</code> that contains 
	 * .zip with OpenDocument meta-information.
	 * @return an <code>OpenDocumentMetadata</code> structure that
	 * represents the file's meta information.
	 */
	public OpenDocumentMetadata analyzeZip(InputStream inputStream) {
		ZipInputStream zipStream = new ZipInputStream(inputStream);
		OpenDocumentMetadata metadataResult = null;

		try {
			while (zipStream.available() == 1) {
				// read possible metaEntry
				ZipEntry metaEntry = zipStream.getNextEntry();
				if (metaEntry != null && "meta.xml".equals(metaEntry.getName())) {
					// if real metaEntry we use content to do real analysis
					metadataResult = analyzeMetadata(zipStream);
					// analyze is made and we can break the loop
					break;
				}
			}
		} catch (IOException ioe) {
		// IO error
		} finally {
			try {
				// and finally we close stream
				zipStream.close();
			} catch (IOException ioe) {
			// intentionally left blank
			}
		}
		return metadataResult;
	}

	/**
	 * Analyze the metadata in a <code>File</code> which is a .zip file.
	 *
	 * @param inputFile a <code>File</code> that contains OpenDocument
	 * meta-information.
	 * @return an <code>OpenDocumentMetadata</code> structure that
	 * represents the file's meta information.
	 */
	public OpenDocumentMetadata analyzeZip(File inputFile) {
		ZipFile zipFile;
		ZipEntry metaEntry;
		InputStream metaStream = null;
		OpenDocumentMetadata metadataResult = null;

		try {
			zipFile = new ZipFile(inputFile);
			metaEntry = zipFile.getEntry("meta.xml");
			if (metaEntry != null) {
				metaStream = zipFile.getInputStream(metaEntry);
			}
		} catch (IOException e) {
			metaStream = null;
		}
		if (metaStream != null) {
			metadataResult = analyzeMetadata(metaStream);
		}
		return metadataResult;
	}

	/**
	 * Analyze the metadata in an <code>InputStream</code>.
	 *
	 * This is a wrapper for the correctly-capitalized
	 * <code>analyzeMetadata()</code> method.
	 * 
	 * @param metaStream
	 * @return 
	 */
	public OpenDocumentMetadata analyzeMetaData(InputStream metaStream) {
		return analyzeMetadata(metaStream);
	}

	/**
	 * Analyze the metadata in a <code>File</code>.
	 *
	 * This is a wrapper for the
	 * <code>analyzeZip( File )</code> method.
	 * 
	 * @param inputFile
	 * @return 
	 */
	public OpenDocumentMetadata analyzeMetadata(File inputFile) {
		return analyzeZip(inputFile);
	}

	/**
	 * Analyze the metadata in a <code>File</code>.
	 *
	 * This is a wrapper for the
	 * <code>analyzeZip( File )</code> method, with "legacy
	 * capitalization of "MetaData".
	 * 
	 * @param inputFile
	 * @return 
	 */
	public OpenDocumentMetadata analyzeMetaData(File inputFile) {
		return analyzeZip(inputFile);
	}

	/**
	 * Put the content of this element into the metadata object.
	 *
	 * <p>The algorithm depends on reflection; element names must
	 * correspond to fields in OpenDocumentMetadata.</p>
	 * <ol>
	 * <li>Convert the element name to a "set" method name.</li>
	 * <li>Get the element content (first child, which should be text).</li>
	 * <li>Invoke the set method with the content as its parameter</li>
	 * </ol>
	 *
	 * @param element the <code>&lt;meta:...&gt;</code> or
	 * <code>&lt;dc:...&gt;</code> element.
	 * @param metadataResult the metadata object to modify.
	 *
	 */
	protected void processElement(Element element, OpenDocumentMetadata metadataResult) {
		Object[] theParameter = new String[1];
		Node textChild;
		String methodName = makeSetMethodName(getBaseName(element));
		try {
			Method setMethod = OpenDocumentMetadata.class.
					getDeclaredMethod(methodName, stringParameter);
			if (setMethod != null) {
				textChild = element.getFirstChild();
				theParameter[0] = (textChild != null)
						? textChild.getNodeValue().trim() : "";
				setMethod.invoke(metadataResult, theParameter);
			}
		} catch (InvocationTargetException e) {
		// no target - this should be fatal, but ignore it
		} catch (IllegalAccessException e) {
		// can't invoke the method, so do nothing
		} catch (NoSuchMethodException e) {
		// no method written to handle this element, so do nothing
		}
	}

	/**
	 * Put the content of the statistic element's
	 * attributes into the metadata object.
	 *
	 * <p>The algorithm depends on reflection; attribute names must
	 * correspond to fields in OpenDocumentMetadata.</p>
	 * <ol>
	 * <li>Convert each attribute name to a "set" method name.</li>
	 * <li>Get the attribute value.</li>
	 * <li>Invoke the set method with the content as its parameter</li>
	 * </ol>
	 * @param element the <code>&lt;meta:document-statistic&gt;</code> element.
	 * @param metadataResult the metadata object to be set.
	 *
	 */
	protected void processStatistic(Element element, OpenDocumentMetadata metadataResult) {
		String attrValue;
		String attrName;
		String methodName;
		Object[] theParameter = new Integer[1];
		NamedNodeMap attr;
		attr = element.getAttributes();
		for (int i = 0; i < attr.getLength(); i++) {
			try {
				attrName = getBaseName(attr.item(i));
				methodName = makeSetMethodName(attrName);
				Method setMethod = OpenDocumentMetadata.class.getDeclaredMethod(methodName, int.class);
				if (setMethod != null) {
					attrValue = attr.item(i).getNodeValue();
					theParameter[0] = (attrValue != null)
							? Integer.valueOf(attrValue) : new Integer(0);
					setMethod.invoke(metadataResult, theParameter);
				}
			} catch (InvocationTargetException e) {
			// no target - this should be fatal, but ignore it
			} catch (IllegalAccessException e) {
			// can't invoke the method, so do nothing
			} catch (NoSuchMethodException e) {
			// no method written to handle this element, so do nothing
			}
		}

	}

	/**
	 * Put the content of this element into the user-defined section
	 * of the metadata object.
	 *
	 * <p>This method presumes that the content of the element is its first
	 * child, which is a text node.</p>
	 *
	 * @param element the <code>&lt;meta:user-defined&gt;</code>
	 * element containing the information.
	 * @param metadataResult the metadata object to modify.
	 *
	 */
	protected void processUserDefined(Element element, OpenDocumentMetadata metadataResult) {
		String dataType;
		String content;
		String key;

		if (element.hasChildNodes()) {
			content = element.getFirstChild().getNodeValue();
			dataType = element.getAttribute(metaNamespace + "value-type");
			dataType = (dataType.equals("")) ? "string" : dataType;

			key = element.getAttribute(metaNamespace + "name");
			if (!key.equals("")) {
				if (dataType.equals("string") || dataType.equals("date")) {
					metadataResult.setUserDefined(key, content);
				} else if (dataType.equals("float")) {
					metadataResult.setUserDefined(key,
							Double.valueOf(content));
				} else if (dataType.equals("boolean")) {
					metadataResult.setUserDefined(key,
							Boolean.valueOf(content));
				} else if (dataType.equals("time")) {
					metadataResult.setUserDefined(key,
							Duration.parseDuration(content));
				}
			}
		}
	}

	/**
	 * Analyzes an Open Document meta file, presumed to be in .zip format.
	 * This is a wrapper for the non-static method.
	 *
	 * @param inputFile the <code>File</code> to analyze.
	 * @return an <code>OpenDocumentMetadata</code> object.
	 */
	public static OpenDocumentMetadata analyzeFile(File inputFile) {
		ODFMetaFileAnalyzer mfa = new ODFMetaFileAnalyzer();
		return mfa.analyzeZip(inputFile);
	}

	/**
	 * Finds the namespace prefixes associated with OpenDocument,
	 * Dublin Core, and OpenDocument meta elements.
	 *
	 * <p>This function presumes that all the namespaces are in the
	 * root element. If they aren't, this breaks.</p>
	 *
	 * @param rootElement the root element of the document.
	 */
	protected void findNamespaces(Element rootElement) {
		NamedNodeMap attributes;
		Node node;
		String value;

		attributes = rootElement.getAttributes();
		for (int i = 0; i < attributes.getLength(); i++) {
			node = attributes.item(i);
			value = node.getNodeValue();

			if (value.equals(DC_URI)) {
				dcNamespace = extractNamespace(node.getNodeName());
			} else if (value.equals(META_URI)) {
				metaNamespace = extractNamespace(node.getNodeName());
			} else if (value.equals(OPENDOCUMENT_URI)) {
				officeNamespace = extractNamespace(node.getNodeName());
			}
		}
	}

	/**
	 * Extract a namespace from a namespace attribute.
	 * @param namespaceAttrName an attribute name in the form
	 *	<code>xmlns:aaaa</code>.
	 * @return the namespace, including the colon separator.
	 */
	protected String extractNamespace(String namespaceAttrName) {
		String result;
		int pos = namespaceAttrName.indexOf(":");

		result = (pos > 0)
				? namespaceAttrName.substring(pos + 1) + ":"
				: "";
		return result;
	}

	/**
	 * Get the "local" name in a non-namespace-aware parser.
	 * @param node the <code>Node</code> whose local name we want.
	 * @return the portion of the name after the <code>:</code>.
	 */
	protected String getBaseName(Node node) {
		String result = node.getNodeName();
		int pos = result.indexOf(":");
		if (pos >= 0) {
			result = result.substring(pos + 1);
		}
		return result;
	}

	/**
	 * Create a set method name corresponding to a meta-element.
	 *
	 * Takes the given name and changes all <code>-<i>letter</i></code>
	 * sequences to (capitalized) <code><i>Letter</i></code>, prepended
	 * by <code>set</code>. Thus, an element name of 
	 * <code>initial-creator</code>
	 * converts to the method name <code>setInitialCreator</code>.
	 *
	 * @param elementName the name of the element to munge.
	 * @return the name of a set method
	 */
	protected String makeSetMethodName(String elementName) {
		String[] part;
		String result;
		int i;
		part = elementName.split("-");
		result = "set";
		for (i = 0; i < part.length; i++) {
			result += part[i].substring(0, 1).toUpperCase() +
					part[i].substring(1);
		}
		return result;
	}
}
